'''
Created on 14 jan 2010

@author: Marcus
'''
import urllib2
import base64
import sys
import types
import re 
import htmlentitydefs

ContentFactory = None

#----------------------------------------------------    
#    Error definitions
#----------------------------------------------------

class ApplicationError(Exception):
    """ General exception class """
    pass

class ParameterError(ApplicationError):
    pass

class ParserError(ApplicationError):
    
    _url = None
    
    def getUrl(self):
        return self._url
    
    URL = property(getUrl, None, None, "Gets or sets the URL associated with this error")
    
    def __init__(self, url, message="No data found", *args):
        self._url = url        
        ApplicationError.__init__(self, message, *args)        

class UserCanceledError(ApplicationError):
    pass

#----------------------------------------------------    
#    Base classes for event handling
#----------------------------------------------------
	
class EventHandler(object):
    """ Object capable of notifying any subscribed listener(s) """
    def __init__(self):
        self._observers = []

    def addListener(self, observer):        
        if not observer in self._observers:
            self._observers.append(observer)

    def removeListener(self, observer):
        try:
            self._observers.remove(observer)
        except ValueError:
            pass

    def _notify(self, *args):
        for observer in self._observers:
            observer(self, *args)

class ProgressEventHandler(EventHandler):
    """ Class capable of notifying a listener of progress """
    
    _progress = 0
    _lastMessage = None    
    
    def __init__(self):
        self._progress = 0
        self._lastMessage = None
        EventHandler.__init__(self)
    
    def _showProgress(self, message=None):
        
        #Always show a message, use last message if none was provided
        if message is not None and message != self._lastMessage:
            self._lastMessage = message   
            self._progress = 0         
        
        self._notify(self._progress % 100, self._lastMessage)
        self._progress += 1  

#----------------------------------------------------    
#    Parser/Site related classes
#----------------------------------------------------
		
class ParserFactory:
    """ Factory class responsible of instantiating correct webparser  
        Override and implement _getDefaultParser to define an initial
        parser. A parser responsible for parsing whats shown in the first
        content list in xbmc....
    """
    
    def _getDefaultParser(self):
        raise NotImplementedError
    
    def getContents(self, uri=None):
        """ Instantiates a parser, parses data and returns a list of contents
        """
        
        parser = self.getParser(uri)
                
        return parser.parse()
    
    def getParser(self, uri=None):
        """ Instantiates a parser """
        
        if not uri:
            return self._getDefaultParser()
        
        field, url = URI.Decode(uri)
        
        if field is None:
            raise ApplicationError("No parser was specified, unable to parse url [%s]." % url)
        
        parser = self.__getParser(field)        
        parser.URL = url
        
        return parser        
      
    def __getParser(self, field):
        """ Instantiates correct parser. 
            TODO: Optimize """
            
        if field is None or field == "":
            raise ParameterError("Invalid parameter field, value is none or empty.")
        
        #Get type info from <class 'utility.WebParser'> string
        info = field.split("'")[1].split(".")
        
        try:
            identifier = getattr(sys.modules[info[0]], info[1])
        except AttributeError:
            raise NameError("%s doesn't exist." % field)
        
        if isinstance(identifier, (types.ClassType, types.TypeType)):
            parser = WebParser()
            parser.__class__ = identifier
            return parser
        
        raise TypeError("%s is not a class." % field)    


class WebParser(object):
    """ Base class for parser logic, override and place your parsing in the onParse method... """
    
    __results = None
    _url = None    
    _source = None
    
    ProgressEvent = None
    ItemAddedEvent = None
    
    def __init__(self):        
        """ Constructor """    
        self.__results = list()        
        self.ProgressEvent = ProgressEventHandler()
        self.ItemAddedEvent = EventHandler()        
    
    def getUrl(self):
        return self._url
    
    def setUrl(self, value):
        self._url = value
    
    URL = property(getUrl, setUrl, None, "Gets or sets the URL to be used by this parser")
    
    def getSource(self):
        return self._source
    
    def setSource(self):
        self._source = value
    
    Source = property(getSource, setSource, None, "Gets or sets the Source data for this parser. Contents at URL...")
    
    def onParse(self, url, data):
        """ Override with your parser logic """
        pass
    
    def onAddItemURL(self, url):
        """ Override to filter URL before it gets added """
        return url
    
    def getDefaultURL(self):
        return None
    
    def parse(self, url=None):
        """ Try to parse data from specified url and returns a collection 
        of any found items """
        
        #Get URL if none was provided as argument    
        if not url and self.URL:
            url = self.URL
            
        if not url:
            url = self.getDefaultURL()
        
        assert url, "No URL provided to parse!" + \
                    "If this is a initial parser, you should override getDefaultURL with a start page URL"
                    
        self._url = url
                
        self._source = self._download(url)
        
        self.ProgressEvent._showProgress("Download complete, parsing contents")
        
        self.onParse(url, self._source)
        
        self.ProgressEvent._showProgress("Contents parsed")
            
        return self.__results    
    
    def _addVideoItem(self, url, title, parser=None, image=None, studio=None, plot=None, genre=None):
        """ Wrapper for adding a VideoItem to results """
        self._add(VideoItem(self.onAddItemURL(url), title, parser, image, studio, plot, genre))
    
    def _addFolderItem(self, url, title, parser, image=None):
        """ Wrapper for adding in ImageItem to results """
        self._add(FolderItem(self.onAddItemURL(url), title, parser, image))
    
    def _addImageItem(self, url, title, parser, image):
        """ Wrapper for adding in ImageItem to results """
        self._add(ImageItem(self.onAddItemURL(url), title, parser, image))
    
    def _add(self, item):
        """ Call this method to add parsed result from this parser """
        
        self.ItemAddedEvent._notify(item)
        self.ProgressEvent._showProgress()
        
        self.__results.append(item)
        
    def _extend(self, items):
        """ Append/extend with a collection of items """
        for item in items:
            self._add(item)
    
    def _download(self, url):
        """ Download data from da webbbb 
        
            TODO: Fix threaded download, with a timeout! This is a possible 
                  spot where the script might get stuck and render xbmc unresponsive!
        """
        blockSize = 1024
        flag = True  
        data = ""        
        request = urllib2.Request(url)
        request.add_header = [('User-Agent', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.9.0.3) Gecko/2008092417 Firefox/3.0.3')]
        
        response = urllib2.urlopen(request)

        while flag:
            inbuff = response.read(blockSize)
            data += inbuff
            self.ProgressEvent._showProgress("Downloading data")            
            flag = len(inbuff) > 0
        
        response.close();
    
        return data
    
    def __str__(self):
        return "%s | %s" % (self.__class__.__name__, self.URL)
		
#----------------------------------------------------    
#    Convenience Classes for URI/Web handling
#----------------------------------------------------
    
class URI:
    """ Methods for internal URI handling, currently contains url to parse and
        the name of the parser class to handle it.
        
        TODO: Support for some kind of metadata, maybee name/value array  
        """
    @staticmethod
    def Encode(parser, url):
        if parser is None: parser = ""
        uri = "%s;%s" % (parser, url)
        return base64.urlsafe_b64encode(uri)
    
    @staticmethod   
    def Decode(uri):
        data = base64.urlsafe_b64decode(uri)        
        data = data.split(";")     
        if data[0] == "": data[0] = None           
        return data
    
    
class Web:
    """ Web related methods """
    
    @staticmethod
    def Unescape(text):
        if text is None: return text
        def Fixup(m):
            text = m.group(0)
            
            #decode named entities this way... XBMC wont accept it through 
            #unichr() and name2codepoint
            if text[:2] != "&#" and text[:1] == "&" :
                text = htmlentitydefs.entitydefs[text[1:-1]]
            
            if text[:2] == "&#":
                # character reference
                try:
                    if text[:3] == "&#x":
                        return unichr(int(text[3:-1], 16))
                    else:
                        return unichr(int(text[2:-1]))
                except ValueError:
                    pass    
                    
            return text # leave as is
        return re.sub("&#?\w+;", Fixup, text)
    
#----------------------------------------------------    
#    Classes for contents found on websites
#----------------------------------------------------
   
class Item(object):
    """ Basic parse result from webpage """

    def __init__(self, url, title, parser):
        
        if url is None:
            raise Exception("Item to add must have an url.")
        
        self.__url      = url
        self.__title    = Web.Unescape(title).strip()
        self.__uri      = URI.Encode(parser, url)

    __url       = None
    __title     = None
    __uri       = None
    
    def getTitle(self):
        return self.__title
    
    def getURL(self):
        return self.__url
    
    def getURI(self):
        return self.__uri
        
    Title   = property(getTitle, None, None, "Title")
    URL     = property(getURL, None, None, "URL")
    URI     = property(getURI, None, None, "URI")
    
    def __str__(self):
        return "%s [%s]" % (self.__title, self.__url)
    
class ImageItem(Item):    
    """ Parse result with an image (img url) """

    def __init__(self, url, title, parser, image, isResource = False):
        Item.__init__(self, url, title, parser)
        self.__image = image
        self.__isResource = isResource        

    __image     = None
    __isResource = False
    
    def getImage(self):
        return self.__image
    
    def getIsResource(self):
        return self.__isResource

    Image = property(getImage, None, None, "Image")
    IsResource = property(getIsResource, None, None, "Is this image a local resource?")
    
class FolderItem(ImageItem):
    pass

class VideoItem(ImageItem):
    """ Parse result with url pointing to a video source  """
    
    def __init__(self, url, title, parser, image, studio=None, plot=None, genre=None):
        ImageItem.__init__(self, url, title, parser, image)
        self.__studio   = Web.Unescape(studio)
        self.__plot     = Web.Unescape(plot)
        self.__genre    = Web.Unescape(genre)
    
    __studio    = None
    __plot      = None
    __genre     = None
    
    def getStudio(self):
        return self.__studio
    def getPlot(self):
        return self.__plot
    def getGenre(self):
        return self.__genre
    
    Studio  = property(getStudio, None, None, "Studio")
    Plot    = property(getPlot, None, None, "Plot")
    Genre   = property(getGenre, None, None, "Genre")    
    
